package scuts;

#if (macro || display)

import haxe.macro.Expr;
import haxe.PosInfos;
import haxe.macro.Type;
import neko.io.File;
import scuts.macro.Cast;
import scuts.macro.Convert;
import scuts.macro.ExtendedContext;
import scuts.macro.Parse;
import scuts.macro.Print;

//import scuts.tools.ExprPrinter;

import haxe.macro.Context;

typedef SType = scuts.macro.Type;

typedef ORequire<T> = ExprRequire<T>;
typedef OExpr = Expr;
typedef OExprArr = Array<Expr>;

using scuts.ArrayTools;
using scuts.Core;



class Overload 
{
  
  static var overloadId = "scutsOverload";
  static var id = 0;
    
  
  public static function applyUsing(instance:Expr, exprs:Array<Expr>, ?pos:PosInfos) {
    // remove nulls
    
    
    
    
    var notNullExprs = [];
    for (e in exprs) {
      var isNull = switch(e.expr) {
        case EConst(c): switch (c) { 
          case CIdent(i): i == "null";
          default: false;
        }
        default:false;
      };
      if (e != null && !isNull) notNullExprs.push(e);
    }

    return apply(notNullExprs, instance, true, false, pos); 
  }
  
  public static function applyStaticConstruct (exprs:Array<Expr>, ?pos:PosInfos) {
    return apply(exprs, null, false, true, pos);
  }
  
  public static function applyInstance(exprs:Array<Expr>, ?pos:PosInfos) {
    var instance = exprs.shift();
    return apply(exprs, instance, pos); 
  }
  
  public static function applyStatic(exprs:Array<Expr>, ?pos:PosInfos) {
    return apply(exprs, pos);
  }
  
  public static function apply(exprs:Array<Expr>, ?instance:Expr, ?isUsing:Bool = false, ?isConstruct:Bool = false, ?pos:PosInfos) 
  {
    if (exprs == null) exprs = [];
    
    
    var p = Context.currentPos();
    
    
    function log(e:Dynamic) {
      var out = File.append("log.txt", false);
      out.writeString(Std.string(e));
      out.close();
    }
    
    
    
    function expr(d:ExprDef) {
      return { expr:d, pos:p };
    }
    
    function error (msg) {
      //throw msg;
      ExtendedContext.error(msg, p);
    }
    
    if (exprs.length == 1) {
      switch (exprs[0].expr) {
        case EDisplayNew(t): log("edisplaynew " + Std.string(exprs[0]));
        case EDisplay(e, isCall): log("edisplay" + Std.string(exprs[0]));
        default: //log(Std.string(exprs[0]));
      }
    }
    
    
    var cl = pos.className;
	var parts = cl.split(".");
	var onlyClassName = parts.last();
	parts.pop();
	var pack = parts;
	
    var fileName = pos.fileName;
    var method = pos.methodName;
    var matcher = ~/([A-Za-z0-9_]*)\.hx/;
    matcher.match(fileName);
    var modulName = matcher.matched(1);
    var oldCl = cl;
	var cl = parts.join(".") + (parts.length > 0 ? "." : "") + (modulName != onlyClassName ? modulName + "." : "") + onlyClassName;
	
	
	var type = 
		if (modulName != onlyClassName) {
			SType.getTypeFromModule(parts.join(".") + (parts.length > 0 ? "." : "") + modulName, onlyClassName);
		}
		else 
			Context.getType(cl);
	
	
    var classType = Context.parse(cl, p);
    
    
    var retType = null;
    
    switch (type) {
    
      case TInst(t, params):
          
          var fields = if (instance != null && !isUsing) {
            t.get().fields.get();
          } else {
            t.get().statics.get();
          }
          
          for (f in fields) {
            if (f.name == method) {
              var metas = f.meta.get();
              for (m in metas) {
                if (m.name == ":" + overloadId) {
                  for (func in m.params) {
                    
                    switch (func.expr) {

                      case EFunction(funcName, f): 
                        var funcs = [];
                        
                        var funcExpr = expr(EReturn(expr(EConst(CIdent("null")))));
                        var funcParams = [];
                        var funcRet = f.ret;
                        
                        var hasVarArgs = false;
                        var newArgs = [];
                        for (i in 0...f.args.length) {
                          var a = f.args[i];
                          if (a.type != null) {
                            switch (a.type) {
                              case TPath(p):
                                // do we have VarArgs in this function signature?
                                if (p.name == "VarArgs" && p.params.length == 1) {
                                  if (i != f.args.length - 1) error("VarArgs<?> must be the last function parameter");
                                  if (p.params.length != 1) error("VarArgs<?> accepts exactly one type parameter");
                                  var argType = switch (p.params[0]) {
                                    case TPType(ct): ct;
                                    default: error("VarArgs doesn't accept const type param"); null;
                                  };
                                  hasVarArgs = true;
                                  // push functions
                                  var argsCopy = newArgs;
                                  
                                  // add function with zero args
                                  var f = expr(EFunction(funcName, {
                                      args: argsCopy,
                                      expr:funcExpr,
                                      params:funcParams,
                                      ret:funcRet
                                    }));
                                    funcs.push(f);
                                  
                                  // add function with 0...x args
                                  for (i in 0...(exprs.length-i)) {
                                    argsCopy = argsCopy.copy();
                                    var arg = {
                                      name:a.name + i,
                                      opt:false,
                                      type:argType,
                                      value:null
                                    }
                                    argsCopy.push(arg);
                                    var f = expr(EFunction(funcName, {
                                      args: argsCopy,
                                      expr:funcExpr,
                                      params:funcParams,
                                      ret:funcRet
                                    }));
                                    
                                    funcs.push(f);
                                  }
                                } else {
                                  newArgs.push(a);
                                }
                              default:newArgs.push(a);
                            }
                          } else {
                            newArgs.push(a);
                          }
                        }
                        if (!hasVarArgs) {
                          
                          // normal function, no VarArgs
                          var func2 = expr(EFunction(funcName, {
                            args: newArgs,
                            expr:funcExpr,
                            params:funcParams,
                            ret:funcRet
                          }));
                          funcs.push(func2);
                        }
                        for (func2 in funcs) {
                          var call = { expr:ECall(func2, exprs), pos:p };
                          if (f.ret == null) 
							if (isConstruct) {
								f.ret = Convert.typeToComplexType(type, p);
							} else {
								error("You must define a return type for each function signature");
							}
                          
                          try {
                            if (f.args.length != exprs.length && !hasVarArgs) throw "args";
						
							try {
								
								retType = Context.typeof(call);
							} catch (e:Dynamic) {
								
								throw e;
							}
                            
                            var field = if (instance != null) {
                              
                              //var field1 = expr(EField(expr(EConst(CType("Reflect"))), "field"));
                              
                              expr(EField(expr(EUntyped(instance)), method));
                            } else {
                              if (isConstruct) {
                                
                                var tp = {
                                  pack:[],
                                  name:"Type",
                                  params:[],
                                  sub: try SType.getModule(t.get()) catch (e:Dynamic) null
                                };
                                expr(EField(expr(EConst(CType("Type"))), "createInstance"));
								
                              } else {
                                expr(EField(expr(EUntyped(classType)), method));
                              }
                            }
                            if (isConstruct) {
                              var args:Array<Dynamic> = exprs.copy();
							  args.unshift(f.ret);
							  var str = "untyped __new__($0" + exprs.foldli(function (acc, e, i) return acc + ",$" + (i + 1), "") + ")";
								  call = Parse.parse(str, args);
							  
                              
							  
                            } else {
                              call = expr(ECall(field, exprs));
                            }
							
							
                            
                            var ret = Cast.typedCast(call, f.ret);
                            return ret;
                          } catch (e:Dynamic) {
                            // current signature does not match, just continue the loop
                          }
                        }
                       
                      default: error(":" + overloadId + " expects a variable amount of anonymous functions");
                    }
                  }
                  
                  var err = "No function signature matches the current call, valid calls are:";
				  for (i in 0...m.params.length) {
                    var func = m.params[i];
                    switch (func.expr) {

                      case EFunction(funcName, f):
						  err += "\n" + Print.functionSignatureStr(f, funcName);
					  default: 
					}
				  }
				  error(err);
                }
                
              }
            }
          }
          
      default: error("error");
      
    }
    
    
    
    error("Invalid call" + exprs);
    return expr(EBlock([]));
  }
  
}
#end